/*
This file is part of the iText (R) project.
Copyright (c) 1998-2025 Apryse Group NV
Authors: Apryse Software.

This program is offered under a commercial and under the AGPL license.
For commercial licensing, contact us at https://itextpdf.com/sales.  For AGPL licensing, see below.

AGPL licensing:
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU Affero General Public License for more details.

You should have received a copy of the GNU Affero General Public License
along with this program.  If not, see <https://www.gnu.org/licenses/>.
*/
using System;
using System.Collections.Generic;
using Microsoft.Extensions.Logging;
using iText.Commons;
using iText.Kernel.Exceptions;
using iText.Kernel.Logs;

namespace iText.Kernel.Pdf {
    /// <summary>
    /// A
    /// <see cref="MemoryLimitsAwareHandler"/>
    /// handles memory allocation and prevents decompressed
    /// pdf streams from occupation of more space than allowed.
    /// </summary>
    /// <remarks>
    /// A
    /// <see cref="MemoryLimitsAwareHandler"/>
    /// handles memory allocation and prevents decompressed
    /// pdf streams from occupation of more space than allowed.
    /// <para />A configured MemoryLimitsAwareHandler can be set as a property of
    /// <see cref="ReaderProperties"/>
    /// instance which is passed to
    /// <see cref="PdfReader"/>.
    /// </remarks>
    /// <seealso cref="ReaderProperties.SetMemoryLimitsAwareHandler(MemoryLimitsAwareHandler)"/>
    public class MemoryLimitsAwareHandler {
        private static readonly ILogger LOGGER = ITextLogManager.GetLogger(typeof(iText.Kernel.Pdf.MemoryLimitsAwareHandler
            ));

        private const int SINGLE_SCALE_COEFFICIENT = 100;

        private const int SUM_SCALE_COEFFICIENT = 500;

        private const int MAX_NUMBER_OF_ELEMENTS_IN_XREF_STRUCTURE = 50000000;

        private const int MIN_LIMIT_FOR_NUMBER_OF_ELEMENTS_IN_XREF_STRUCTURE = 500000;

        private const int SINGLE_DECOMPRESSED_PDF_STREAM_MIN_SIZE = int.MaxValue / 100;

        private const long SUM_OF_DECOMPRESSED_PDF_STREAMS_MIN_SIZE = int.MaxValue / 20;

        private const long MAX_X_OBJECTS_SIZE_PER_PAGE = 1024L * 1024L * 1024L * 3;

        private int maxSizeOfSingleDecompressedPdfStream;

        private long maxSizeOfDecompressedPdfStreamsSum;

        private int maxNumberOfElementsInXrefStructure;

        private long maxXObjectsSizePerPage;

        private long allMemoryUsedForDecompression = 0;

        private long memoryUsedForCurrentPdfStreamDecompression = 0;

//\cond DO_NOT_DOCUMENT
        internal bool considerCurrentPdfStream = false;
//\endcond

        /// <summary>
        /// Creates a
        /// <see cref="MemoryLimitsAwareHandler"/>
        /// which will be used to handle decompression of pdf streams.
        /// </summary>
        /// <remarks>
        /// Creates a
        /// <see cref="MemoryLimitsAwareHandler"/>
        /// which will be used to handle decompression of pdf streams.
        /// The max allowed memory limits will be generated by default.
        /// </remarks>
        public MemoryLimitsAwareHandler()
            : this(SINGLE_DECOMPRESSED_PDF_STREAM_MIN_SIZE, SUM_OF_DECOMPRESSED_PDF_STREAMS_MIN_SIZE, MAX_NUMBER_OF_ELEMENTS_IN_XREF_STRUCTURE
                , MAX_X_OBJECTS_SIZE_PER_PAGE) {
        }

        /// <summary>
        /// Creates a
        /// <see cref="MemoryLimitsAwareHandler"/>
        /// which will be used to handle decompression of pdf streams.
        /// </summary>
        /// <remarks>
        /// Creates a
        /// <see cref="MemoryLimitsAwareHandler"/>
        /// which will be used to handle decompression of pdf streams.
        /// The max allowed memory limits will be generated by default, based on the size of the document.
        /// </remarks>
        /// <param name="documentSize">the size of the document, which is going to be handled by iText.</param>
        public MemoryLimitsAwareHandler(long documentSize)
            : this((int)CalculateDefaultParameter(documentSize, SINGLE_SCALE_COEFFICIENT, SINGLE_DECOMPRESSED_PDF_STREAM_MIN_SIZE
                ), CalculateDefaultParameter(documentSize, SUM_SCALE_COEFFICIENT, SUM_OF_DECOMPRESSED_PDF_STREAMS_MIN_SIZE
                ), CalculateMaxElementsInXref(documentSize), MAX_X_OBJECTS_SIZE_PER_PAGE) {
        }

        private MemoryLimitsAwareHandler(int maxSizeOfSingleDecompressedPdfStream, long maxSizeOfDecompressedPdfStreamsSum
            , int maxNumberOfElementsInXrefStructure, long maxXObjectsSizePerPage) {
            this.maxSizeOfSingleDecompressedPdfStream = maxSizeOfSingleDecompressedPdfStream;
            this.maxSizeOfDecompressedPdfStreamsSum = maxSizeOfDecompressedPdfStreamsSum;
            this.maxNumberOfElementsInXrefStructure = maxNumberOfElementsInXrefStructure;
            this.maxXObjectsSizePerPage = maxXObjectsSizePerPage;
        }

        /// <summary>
        /// Creates a new instance of
        /// <see cref="MemoryLimitsAwareHandler"/>
        /// by copying settings from this instance
        /// of
        /// <see cref="MemoryLimitsAwareHandler"/>.
        /// </summary>
        /// <returns>
        /// a new instance of
        /// <see cref="MemoryLimitsAwareHandler"/>.
        /// </returns>
        public virtual iText.Kernel.Pdf.MemoryLimitsAwareHandler CreateNewInstance() {
            iText.Kernel.Pdf.MemoryLimitsAwareHandler to = new iText.Kernel.Pdf.MemoryLimitsAwareHandler();
            to.maxSizeOfSingleDecompressedPdfStream = this.maxSizeOfSingleDecompressedPdfStream;
            to.maxSizeOfDecompressedPdfStreamsSum = this.maxSizeOfDecompressedPdfStreamsSum;
            to.maxNumberOfElementsInXrefStructure = this.maxNumberOfElementsInXrefStructure;
            to.maxXObjectsSizePerPage = this.maxXObjectsSizePerPage;
            if (this.GetType() != typeof(iText.Kernel.Pdf.MemoryLimitsAwareHandler)) {
                LOGGER.LogWarning(KernelLogMessageConstant.MEMORYLIMITAWAREHANDLER_OVERRIDE_CREATENEWINSTANCE_METHOD);
            }
            return to;
        }

        /// <summary>Gets the maximum allowed size which can be occupied by a single decompressed pdf stream.</summary>
        /// <returns>the maximum allowed size which can be occupied by a single decompressed pdf stream.</returns>
        public virtual int GetMaxSizeOfSingleDecompressedPdfStream() {
            return maxSizeOfSingleDecompressedPdfStream;
        }

        /// <summary>Sets the maximum allowed size which can be occupied by a single decompressed pdf stream.</summary>
        /// <remarks>
        /// Sets the maximum allowed size which can be occupied by a single decompressed pdf stream.
        /// This value correlates with maximum heap size. This value should not exceed limit of the heap size.
        /// <para />iText will throw an exception if during decompression a pdf stream which was identified as
        /// requiring memory limits awareness occupies more memory than allowed.
        /// </remarks>
        /// <param name="maxSizeOfSingleDecompressedPdfStream">
        /// the maximum allowed size which can be occupied by a single
        /// decompressed pdf stream.
        /// </param>
        /// <returns>
        /// this
        /// <see cref="MemoryLimitsAwareHandler"/>
        /// instance.
        /// </returns>
        /// <seealso cref="IsMemoryLimitsAwarenessRequiredOnDecompression(PdfArray)"/>
        public virtual iText.Kernel.Pdf.MemoryLimitsAwareHandler SetMaxSizeOfSingleDecompressedPdfStream(int maxSizeOfSingleDecompressedPdfStream
            ) {
            this.maxSizeOfSingleDecompressedPdfStream = maxSizeOfSingleDecompressedPdfStream;
            return this;
        }

        /// <summary>Gets the maximum allowed size which can be occupied by all decompressed pdf streams.</summary>
        /// <returns>the maximum allowed size value which streams may occupy</returns>
        public virtual long GetMaxSizeOfDecompressedPdfStreamsSum() {
            return maxSizeOfDecompressedPdfStreamsSum;
        }

        /// <summary>Sets the maximum allowed size which can be occupied by all decompressed pdf streams.</summary>
        /// <remarks>
        /// Sets the maximum allowed size which can be occupied by all decompressed pdf streams.
        /// This value can be limited by the maximum expected PDF file size when it's completely decompressed.
        /// Setting this value correlates with the maximum processing time spent on document reading
        /// <para />iText will throw an exception if during decompression pdf streams which were identified as
        /// requiring memory limits awareness occupy more memory than allowed.
        /// </remarks>
        /// <param name="maxSizeOfDecompressedPdfStreamsSum">
        /// he maximum allowed size which can be occupied by all decompressed pdf
        /// streams.
        /// </param>
        /// <returns>
        /// this
        /// <see cref="MemoryLimitsAwareHandler"/>
        /// instance.
        /// </returns>
        /// <seealso cref="IsMemoryLimitsAwarenessRequiredOnDecompression(PdfArray)"/>
        public virtual iText.Kernel.Pdf.MemoryLimitsAwareHandler SetMaxSizeOfDecompressedPdfStreamsSum(long maxSizeOfDecompressedPdfStreamsSum
            ) {
            this.maxSizeOfDecompressedPdfStreamsSum = maxSizeOfDecompressedPdfStreamsSum;
            return this;
        }

        /// <summary>
        /// Performs a check if the
        /// <see cref="PdfStream"/>
        /// with provided setup of the filters requires
        /// memory limits awareness during decompression.
        /// </summary>
        /// <param name="filters">
        /// is an
        /// <see cref="PdfArray"/>
        /// of names of filters
        /// </param>
        /// <returns>true if PDF stream is suspicious and false otherwise</returns>
        public virtual bool IsMemoryLimitsAwarenessRequiredOnDecompression(PdfArray filters) {
            HashSet<PdfName> filterSet = new HashSet<PdfName>();
            for (int index = 0; index < filters.Size(); index++) {
                PdfName filterName = filters.GetAsName(index);
                if (!filterSet.Add(filterName)) {
                    return true;
                }
            }
            return false;
        }

        /// <summary>Gets maximum number of elements in xref structure.</summary>
        /// <returns>maximum number of elements in xref structure.</returns>
        public virtual int GetMaxNumberOfElementsInXrefStructure() {
            return maxNumberOfElementsInXrefStructure;
        }

        /// <summary>Gets maximum page size.</summary>
        /// <returns>maximum page size.</returns>
        public virtual long GetMaxXObjectsSizePerPage() {
            return maxXObjectsSizePerPage;
        }

        /// <summary>Sets maximum page size.</summary>
        /// <param name="maxPageSize">maximum page size.</param>
        public virtual void SetMaxXObjectsSizePerPage(long maxPageSize) {
            this.maxXObjectsSizePerPage = maxPageSize;
        }

        /// <summary>Sets maximum number of elements in xref structure.</summary>
        /// <param name="maxNumberOfElementsInXrefStructure">maximum number of elements in xref structure.</param>
        public virtual void SetMaxNumberOfElementsInXrefStructure(int maxNumberOfElementsInXrefStructure) {
            this.maxNumberOfElementsInXrefStructure = maxNumberOfElementsInXrefStructure;
        }

        /// <summary>Performs a check of possible extension of xref structure.</summary>
        /// <param name="requestedCapacity">capacity to which we need to expand xref array.</param>
        public virtual void CheckIfXrefStructureExceedsTheLimit(int requestedCapacity) {
            // Objects in xref structures are using 1-based indexes, so to store maxNumberOfElementsInXrefStructure
            // amount of elements we need maxNumberOfElementsInXrefStructure + 1 capacity.
            if (requestedCapacity - 1 > maxNumberOfElementsInXrefStructure || requestedCapacity < 0) {
                throw new MemoryLimitsAwareException(KernelExceptionMessageConstant.XREF_STRUCTURE_SIZE_EXCEEDED_THE_LIMIT
                    );
            }
        }

        public virtual void CheckIfPageSizeExceedsTheLimit(long totalXObjectsSize) {
            if (totalXObjectsSize > maxXObjectsSizePerPage) {
                throw new MemoryLimitsAwareException(KernelExceptionMessageConstant.TOTAL_XOBJECT_SIZE_ONE_PAGE_EXCEEDED_THE_LIMIT
                    );
            }
        }

        /// <summary>Calculate max number of elements allowed in xref table based on the size of the document, achieving max limit at 100MB.
        ///     </summary>
        /// <param name="documentSizeInBytes">document size in bytes.</param>
        /// <returns>calculated limit.</returns>
        protected internal static int CalculateMaxElementsInXref(long documentSizeInBytes) {
            int maxDocSizeForMaxLimit = MAX_NUMBER_OF_ELEMENTS_IN_XREF_STRUCTURE / MIN_LIMIT_FOR_NUMBER_OF_ELEMENTS_IN_XREF_STRUCTURE;
            int documentSizeInMb = Math.Max(1, Math.Min((int)documentSizeInBytes / (1024 * 1024), maxDocSizeForMaxLimit
                ));
            return documentSizeInMb * MIN_LIMIT_FOR_NUMBER_OF_ELEMENTS_IN_XREF_STRUCTURE;
        }

//\cond DO_NOT_DOCUMENT
        /// <summary>Considers the number of bytes which are occupied by the decompressed pdf stream.</summary>
        /// <remarks>
        /// Considers the number of bytes which are occupied by the decompressed pdf stream.
        /// If memory limits have not been faced, throws an exception.
        /// </remarks>
        /// <param name="numOfOccupiedBytes">the number of bytes which are occupied by the decompressed pdf stream.</param>
        /// <returns>
        /// this
        /// <see cref="MemoryLimitsAwareHandler"/>
        /// instance.
        /// </returns>
        /// <seealso cref="iText.Kernel.Exceptions.MemoryLimitsAwareException"/>
        internal virtual iText.Kernel.Pdf.MemoryLimitsAwareHandler ConsiderBytesOccupiedByDecompressedPdfStream(long
             numOfOccupiedBytes) {
            if (considerCurrentPdfStream && memoryUsedForCurrentPdfStreamDecompression < numOfOccupiedBytes) {
                memoryUsedForCurrentPdfStreamDecompression = numOfOccupiedBytes;
                if (memoryUsedForCurrentPdfStreamDecompression > maxSizeOfSingleDecompressedPdfStream) {
                    throw new MemoryLimitsAwareException(KernelExceptionMessageConstant.DURING_DECOMPRESSION_SINGLE_STREAM_OCCUPIED_MORE_MEMORY_THAN_ALLOWED
                        );
                }
            }
            return this;
        }
//\endcond

//\cond DO_NOT_DOCUMENT
        /// <summary>Begins handling of current pdf stream decompression.</summary>
        /// <returns>
        /// this
        /// <see cref="MemoryLimitsAwareHandler"/>
        /// instance.
        /// </returns>
        internal virtual iText.Kernel.Pdf.MemoryLimitsAwareHandler BeginDecompressedPdfStreamProcessing() {
            EnsureCurrentStreamIsReset();
            considerCurrentPdfStream = true;
            return this;
        }
//\endcond

//\cond DO_NOT_DOCUMENT
        /// <summary>Ends handling of current pdf stream decompression.</summary>
        /// <remarks>
        /// Ends handling of current pdf stream decompression.
        /// If memory limits have not been faced, throws an exception.
        /// </remarks>
        /// <returns>
        /// this
        /// <see cref="MemoryLimitsAwareHandler"/>
        /// instance.
        /// </returns>
        /// <seealso cref="iText.Kernel.Exceptions.MemoryLimitsAwareException"/>
        internal virtual iText.Kernel.Pdf.MemoryLimitsAwareHandler EndDecompressedPdfStreamProcessing() {
            allMemoryUsedForDecompression += memoryUsedForCurrentPdfStreamDecompression;
            if (allMemoryUsedForDecompression > maxSizeOfDecompressedPdfStreamsSum) {
                throw new MemoryLimitsAwareException(KernelExceptionMessageConstant.DURING_DECOMPRESSION_MULTIPLE_STREAMS_IN_SUM_OCCUPIED_MORE_MEMORY_THAN_ALLOWED
                    );
            }
            EnsureCurrentStreamIsReset();
            considerCurrentPdfStream = false;
            return this;
        }
//\endcond

//\cond DO_NOT_DOCUMENT
        internal virtual long GetAllMemoryUsedForDecompression() {
            return allMemoryUsedForDecompression;
        }
//\endcond

        private static long CalculateDefaultParameter(long documentSize, int scale, long min) {
            long result = documentSize * scale;
            if (result < min) {
                result = min;
            }
            if (result > min * scale) {
                result = min * scale;
            }
            return result;
        }

        private void EnsureCurrentStreamIsReset() {
            memoryUsedForCurrentPdfStreamDecompression = 0;
        }
    }
}
